import copy

import uuid
import math

import cv2
from PIL import Image, ImageFont, ImageDraw
import aggdraw
import numpy as np
from components import VolumeInfo
from components.CoordTransfer import CoordTransfer
from utils.util import HitCheck, check_is_error_draw, calc_vector_angle
from type import EnumMeasureStatus, EnumShape, EnumMouseOp

r_color = (255, 0, 0)
g_color = (0, 255, 0)
b_color = (0, 0, 255)
endpoint_ball_radius = 5
line_width = 4
font_draw = ImageFont.truetype('arial.ttf', size=14)
every_line_offset = 20

inactive_text_color = (255, 0, 255)
active_text_color = (0, 128, 0)

inactive_pen = aggdraw.Pen((255, 0, 255), 2, opacity=255)
active_pen = aggdraw.Pen('green', 3, opacity=255)


class Draw:
    @staticmethod
    def draw_multi_lines_text(drawer, desc, start_point, which_pen):
        if desc is None or len(desc) == 0:
            return
        ans = []
        for index, item in enumerate(desc):
            line = ''
            if type(item) == dict:
                key = item['attr']
                value = item['value']
                line = f'{key}: {value}'
            elif type(item) == list:
                for index1, item1 in enumerate(item):
                    key1 = item1['attr']
                    value1 = item1['value']
                    line = line + f' {key1}: {value1}'
            ans.append(line)
        for index1, line1 in enumerate(ans):
            current_posi = (int(start_point[0]), int(start_point[1] - every_line_offset * index1))
            Draw.draw_text(drawer, line1, current_posi, which_pen)

    @staticmethod
    def draw_line(drawer, start, end, which_pen='inactive', color=r_color):
        if which_pen == 'inactive':
            drawer.line((int(start[0]), int(start[1]), int(end[0]), int(end[1])), inactive_pen)
        else:
            drawer.line((int(start[0]), int(start[1]), int(end[0]), int(end[1])), active_pen)

    @staticmethod
    def draw_circle(drawer, center_x, center_y, which_pen='inactive'):
        bounding = (center_x - endpoint_ball_radius, center_y - endpoint_ball_radius, center_x + endpoint_ball_radius,
                    center_y + endpoint_ball_radius)
        Draw.draw_ellipse(drawer, bounding, which_pen)

    @staticmethod
    def draw_ellipse(drawer, bounding, which_pen):
        if which_pen == 'inactive':
            drawer.ellipse(bounding, inactive_pen)
        else:
            drawer.ellipse(bounding, active_pen)

    @staticmethod
    def draw_rect():
        pass

    @staticmethod
    def draw_text(drawer, content, posi, which_pen):
        if content is None:
            return
        font = font_draw
        if which_pen == 'inactive':
            drawer.text(posi, content, font=font, fill=inactive_text_color)
        else:
            drawer.text(posi, content, font=font, fill=active_text_color)


class MeasureRecord:
    def __init__(self, measure_type: EnumShape):
        target_points_count = 0
        match measure_type:
            case EnumShape.line.value:
                target_points_count = 2
            case EnumShape.angle.value:
                target_points_count = 3
            case EnumShape.rect.value | EnumShape.ellipse.value:
                target_points_count = 4
        self.id = str(uuid.uuid1())
        self.status: EnumMeasureStatus.value = EnumMeasureStatus.create.value
        self.points = []  # dicom 原始坐标下的点。
        self.measure_type: EnumShape = measure_type
        self.target_points_count = target_points_count
        self.desc = None
        # 正在编辑的点的索引。 -2是没有正在编辑的点， -1 为整体编辑
        # 0~为正在编辑的索引, 如果索引 >= target_points_count。其实是编辑完成了.
        self.editing_point_index = 0
        self.screen_points = []  # 在屏幕下的点的坐标。

    def update_latest_point(self, point):
        if len(self.points) == 0:
            return
        if self.editing_point_index == -2:
            return
        match self.measure_type:
            case EnumShape.rect.value | EnumShape.ellipse.value:
                self.points[self.editing_point_index] = point
                x, y = point
                match self.editing_point_index:
                    case 0:
                        self.points[1] = (self.points[1][0], y)
                        self.points[3] = (x, self.points[3][1])

                    case 1:
                        self.points[0] = (self.points[0][0], y)
                        self.points[2] = (x, self.points[2][1])

                    case 2:
                        self.points[1] = (x, self.points[1][1])
                        self.points[3] = (self.points[3][0], y)

                    case 3:
                        self.points[0] = (x, self.points[0][1])
                        self.points[2] = (self.points[2][0], y)
            case _:
                self.points[self.editing_point_index] = point

    def confirm_a_point(self, point):
        match self.measure_type:
            case EnumShape.rect.value | EnumShape.ellipse.value:
                if len(self.points) == 0:
                    self.points = [] + [point, point, point, point]
                    self.editing_point_index = 2
            case _:
                if len(self.points) == 0:
                    self.points.append(point)
                    self.points.append(point)
                else:
                    self.points.append(point)
                self.editing_point_index = self.editing_point_index + 1

    def check_measure_status(self):
        finished = False
        has_error = False
        if self.status == EnumMeasureStatus.selected.value or len(self.points) < self.target_points_count:
            return finished, has_error
        finished = True
        has_error, reason = check_is_error_draw(self)
        if reason is not None:
            print(reason)
        return finished, has_error


# key 是当前所在Index值， value是 measureId数组
class MeasureDoneDict:
    def __init__(self):
        self.dict = dict()

    def append_a_record(self, key, record):
        if self.dict.__contains__(key):
            arr = self.dict.get(key)
            arr.append(record)
        else:
            self.dict.setdefault(key, [record])

    def remove_a_record(self, key, record_id):
        if self.dict.__contains__(key):
            arr = self.dict.get(key)
            arr.remove(record_id)


class NewMeasureLayer:
    def __init__(self, view_type, dicom_tag_info, volume_info):
        self.view_type = view_type
        self.dicom_tag_info = dicom_tag_info
        self.volume_info: VolumeInfo = volume_info
        self.drawer = None
        self.text_drawer = None
        self.draw_image = None

        self.size = (0, 0)  # h,w
        # measure的信息，已经完成的, measureId => measureRecord
        self.measure_done_dict = dict()
        self.measuring: None | MeasureRecord = None
        # key 是当前影像所在的norm_index, val 是 [measureId]
        self.norm_index_done_dict = MeasureDoneDict()

    def clear_measure(self):
        self.measure_done_dict = dict()
        self.measuring = None
        self.norm_index_done_dict = MeasureDoneDict()

    def set_layer_size(self):
        png_size = self.volume_info.view_info[self.view_type].png_size
        self.size = (png_size[0], png_size[1])

    def resize(self):
        self.set_layer_size()
        self.draw_image = None

    def init_layer(self):
        self.set_layer_size()
        height, width = self.size
        # 如果图层已存在，不用每次重新创建
        if self.draw_image is None:
            self.draw_image = Image.new('RGBA', (width, height), (0, 0, 0, 0))
            self.drawer = aggdraw.Draw(self.draw_image)
            self.text_drawer = ImageDraw.Draw(self.draw_image)
        else:
            # 清空画布
            self.draw_image.paste((0, 0, 0, 0), (0, 0, self.size[1], self.size[0]))
            self.drawer = aggdraw.Draw(self.draw_image)
            self.text_drawer = ImageDraw.Draw(self.draw_image)

    def get_layer(self):
        self.init_layer()
        self.draw_all_records()
        exist_measure_flag = self.check_exist_measure()
        if exist_measure_flag:
            return self.draw_image
        return None

    def check_exist_measure(self):
        if self.measuring is not None:
            return True
        done_id_arr = self.get_current_done_measure_ids()
        return len(done_id_arr) != 0

    def create_measuring(self, measure_type: EnumShape):
        self.measuring = MeasureRecord(measure_type=measure_type)

    def draw_all_records(self):
        self.draw_measure_done()
        self.draw_measuring()
        self.drawer.flush()

        self.draw_measure_done_desc()
        self.draw_measuring_desc()

    def draw_measure_done_desc(self):
        current_norm_index = self.get_current_norm_index()
        measure_done_id_arr = self.norm_index_done_dict.dict.get(current_norm_index)
        if measure_done_id_arr is None:
            return
        for id_done in measure_done_id_arr:
            measure_record = self.measure_done_dict.get(id_done)
            self.draw_a_measure_desc(measure_record, 'inactive')

    def draw_measuring_desc(self):
        if self.measuring is None:
            return
        self.draw_a_measure_desc(self.measuring, 'active')

    def draw_measure_done(self):
        current_norm_index = self.get_current_norm_index()
        measure_done_id_arr = self.norm_index_done_dict.dict.get(current_norm_index)
        if measure_done_id_arr is None:
            return
        for id_done in measure_done_id_arr:
            measure_record = self.measure_done_dict.get(id_done)
            self.draw_a_measure_record(measure_record, 'inactive')

    def draw_measuring(self):
        if self.measuring is None:
            return
        # self.update_measuring_desc()
        desc = self.get_desc_by_shape(self.measuring)
        if desc is None:
            return
        self.measuring.desc = desc
        self.draw_a_measure_record(self.measuring, 'active')

    def draw_a_measure_record(self, measure_record: MeasureRecord, pen_status):
        points = measure_record.points
        status = measure_record.status

        measure_type = measure_record.measure_type
        draw_circle_flag = [EnumMeasureStatus.create.value, EnumMeasureStatus.selected.value].__contains__(status)
        screen_points = [self.dicom_2_screen_position(each_point) for each_point in points]
        measure_record.screen_points = screen_points
        match measure_type:
            case EnumShape.ellipse.value:
                p0 = screen_points[0]
                p2 = screen_points[2]
                left = (min(p0[0], p2[0]), min(p0[1], p2[1]))
                right = (max(p0[0], p2[0]), max(p0[1], p2[1]))
                Draw.draw_ellipse(self.drawer, left + right, pen_status)
                for index, point in enumerate(screen_points):
                    if draw_circle_flag:
                        Draw.draw_circle(self.drawer, point[0], point[1], pen_status)

            case _:
                for index, point in enumerate(screen_points):
                    if draw_circle_flag:
                        Draw.draw_circle(self.drawer, point[0], point[1], pen_status)
                    if index != len(screen_points) - 1:
                        next_point = screen_points[index + 1]
                        Draw.draw_line(self.drawer, point, next_point, pen_status)
                    elif index == len(screen_points) - 1 and measure_record.measure_type == EnumShape.rect.value:
                        next_point = screen_points[0]
                        Draw.draw_line(self.drawer, point, next_point, pen_status)

    def draw_a_measure_desc(self, measure_record: MeasureRecord, status):
        measure_type = measure_record.measure_type
        desc = measure_record.desc
        if desc is None:
            return
        screen_points = measure_record.screen_points
        if len(screen_points) < 2:
            return
        turn_point = screen_points[1]
        match measure_type:
            case EnumShape.ellipse.value | EnumShape.rect.value:
                Draw.draw_multi_lines_text(self.text_drawer, desc, turn_point, status)
            case _:
                Draw.draw_text(self.text_drawer, desc, turn_point, status)

    def handle_mouse_down(self, measure_type: EnumShape, position):
        if self.measuring is not None and self.measuring.status == EnumMeasureStatus.create.value:
            self.confirm_a_point(measure_type, position)
            return

        # 1. 如果当前有selected状态的measure，优先判断是否命中该状态下的点。
        # 2. 如果没有命中。 判断是否命中某个measure
        if self.measuring is not None and self.measuring.status == EnumMeasureStatus.selected.value:
            editing_index = HitCheck.hit_point(position, self.measuring.points)
            self.measuring.editing_point_index = editing_index
            if editing_index != -2:
                print(f'命中了控制点 {editing_index}')
                return
            else:
                self.move_measuring_to_done()
        hit_flag, hit_measure_id = self.check_down_in_which_shape(position)
        if hit_flag is False:
            self.confirm_a_point(measure_type, position)

    def check_down_in_which_shape(self, click_point):
        # 先检查是否命中正在绘制中的。
        # 再检查是否命中已经完成得绘制。
        flag = False
        hit_measure_id = None

        if self.measuring is not None and len(self.measuring.points) == self.measuring.target_points_count:
            points = self.measuring.points
            measure_type = self.measuring.measure_type
            flag = HitCheck.hit_line(click_point, measure_type, points)
            hit_measure_id = self.measuring.id

        if flag is False:
            measure_done_ids = self.get_current_done_measure_ids()
            for measure_done_id in measure_done_ids:
                record: MeasureRecord = self.measure_done_dict.get(measure_done_id)
                points = record.points
                measure_type = record.measure_type
                flag = HitCheck.hit_line(click_point, measure_type, points)
                if flag:
                    hit_measure_id = record.id
                    # 点击的在已有图形中，将点中的图像加入正在绘制中的。
                    if self.measuring is not None and self.measuring.id != hit_measure_id:
                        self.move_measuring_to_done()
                    self.move_measured_to_measuring(measure_done_id)
                    break
        return flag, hit_measure_id

    def move_measured_to_measuring(self, measure_id):
        record: MeasureRecord = self.measure_done_dict.get(measure_id)
        self.measuring = copy.deepcopy(record)
        self.measuring.status = EnumMeasureStatus.selected.value
        self.measure_done_dict.pop(measure_id)
        current_norm_index = self.get_current_norm_index()
        self.norm_index_done_dict.remove_a_record(current_norm_index, measure_id)

    def handle_mouse_op(self, mouse_op: EnumMouseOp, screen_position, measure_type: EnumShape):
        origin_point = self.screen_coord_to_origin_voxel(screen_position)
        match mouse_op:
            case EnumMouseOp.down.value:
                print('down')
                self.handle_mouse_down(measure_type, origin_point)
            case EnumMouseOp.move.value:
                pass
            case EnumMouseOp.drag.value:
                print('drag')
                if self.measuring is not None:
                    self.handle_drag(measure_type, origin_point)
            case EnumMouseOp.up.value:
                print('up')
                if self.measuring is not None:
                    self.check_measuring_finish()

    def screen_coord_to_origin_voxel(self, point):
        original_point = self.volume_info.screen_coord_to_origin_voxel(self.view_type, point[0], point[1])
        return round(original_point[1]), round(original_point[0])

    def dicom_2_screen_position(self, point):
        screen_position = self.volume_info.origin_voxel_coord_to_screen_coord(self.view_type, point)
        return screen_position

    def confirm_a_point(self, measure_type, point):
        if self.measuring is None:
            self.create_measuring(measure_type=measure_type)

        match self.measuring.status:
            case EnumMeasureStatus.create.value:
                self.measuring.confirm_a_point(point)

    def check_measuring_finish(self):
        finished, has_error = self.measuring.check_measure_status()
        if finished is False:
            return
        if has_error:
            self.clear_measuring()
        else:
            self.move_measuring_to_done()

    def get_current_norm_index(self):
        return self.volume_info.get_current_slice_index(self.view_type)

    def move_measuring_to_done(self):
        measuring = copy.deepcopy(self.measuring)
        measuring.status = EnumMeasureStatus.done.value
        self.measure_done_dict.setdefault(measuring.id, measuring)
        current_norm_index = self.get_current_norm_index()
        self.norm_index_done_dict.append_a_record(current_norm_index, measuring.id)
        self.clear_measuring()

    # 获取当前slice的所有已经绘制的图像
    def get_current_done_measure_ids(self):
        current_norm_index = self.get_current_norm_index()
        current_norm_index_done = self.norm_index_done_dict.dict.get(current_norm_index)
        if current_norm_index_done is None:
            return []
        else:
            return current_norm_index_done

    def clear_measuring(self):
        self.measuring = None

    def handle_drag(self, measure_type, point):
        self.measuring.update_latest_point(point)

    def get_desc_by_shape(self, measure_record: MeasureRecord):
        shape = measure_record.measure_type
        points = measure_record.points
        desc = ''
        match shape:
            case EnumShape.ellipse.value:
                hu_array = self.volume_info.get_current_dicom_data(self.view_type)
                spacing = self.volume_info.get_spacing(self.view_type)
                x_mm_per_px, y_mm_per_px = spacing

                p0 = points[0]
                p2 = points[2]

                # 假设椭圆的参数：中心 (cx, cy), 长半轴 a, 短半轴 b
                cx, cy = round((p0[0] + p2[0]) / 2), round((p0[1] + p2[1]) / 2)  # 椭圆中心
                a, b = round(abs(p0[0] - p2[0]) * x_mm_per_px / 2), round(
                    abs(p0[1] - p2[1]) * y_mm_per_px / 2)  # 长半轴和短半轴的长度

                area = round(np.pi * a * b)
                mask = np.zeros_like(hu_array, dtype=np.uint8)
                cv2.ellipse(mask, (cx, cy), (a, b), 0, 0, 360, 1, thickness=-1)
                pixels_in_roi = hu_array[mask == 1]
                if area < 10 or len(pixels_in_roi) <= 1:
                    return None

                mean_val = np.mean(pixels_in_roi)
                stddev_val = np.std(pixels_in_roi)
                max_val = np.max(pixels_in_roi)
                min_val = np.min(pixels_in_roi)

                mean = round(mean_val, 2)
                min_val = round(min_val, 2)
                max_val = round(max_val, 2)
                stddev = round(stddev_val, 2)

                area_str = f'{area / 100} cm²'
                w_str = f'{a * 2} mm'
                h_str = f'{b * 2} mm'
                ans = [
                    {
                        'attr': 'Mean',
                        'value': mean
                    },
                    {
                        'attr': 'Min',
                        'value': min_val
                    },
                    {
                        'attr': 'Max',
                        'value': max_val
                    },
                    {
                        'attr': 'StdDev',
                        'value': stddev
                    },
                    {
                        'attr': 'Area',
                        'value': area_str
                    },
                    [
                        {
                            'attr': 'W',
                            'value': w_str
                        },
                        {
                            'attr': 'H',
                            'value': h_str
                        },
                    ]
                ]
                return ans
            case EnumShape.rect.value:
                x1, y1 = points[0]
                x2, y2 = points[2]
                if y1 < y2:
                    y_from = y1
                    y_to = y2
                else:
                    y_from = y2
                    y_to = y1

                if x1 < x2:
                    x_from = x1
                    x_to = x2
                else:
                    x_from = x2
                    x_to = x1

                hu_array = self.volume_info.get_current_dicom_data(self.view_type)
                ct_values = np.array(hu_array)
                h, w = ct_values.shape
                padding_top = max(0, -y_from)  # 如果 y_from 为负，需在顶部填充
                padding_left = max(0, -x_from)  # 如果 x_from 为负，需在左侧填充
                padding_right = max(0, x_to - w)
                padding_bottom = max(0, y_to - h)

                y_from = max(0, y_from)
                y_to = max(0, y_to) + padding_top
                x_from = max(0, x_from)
                x_to = max(0, x_to) + padding_left

                padding_ct_values = np.pad(ct_values, ((padding_top, padding_bottom), (padding_left, padding_right)),
                                           mode='constant',
                                           constant_values=-1000)

                region_ct_values = padding_ct_values[y_from:y_to, x_from:x_to]
                spacing = self.volume_info.get_spacing(self.view_type)
                x_mm_per_px, y_mm_per_px = spacing
                # 计算宽度 W 和高度 H
                W = round(region_ct_values.shape[1] * x_mm_per_px, 2)  # 矩形的宽度
                H = round(region_ct_values.shape[0] * y_mm_per_px, 2)  # 矩形的高度
                if W * H < 10:
                    return None

                # 计算统计数据
                mean_val = np.mean(region_ct_values)
                stddev_val = np.std(region_ct_values)
                max_val = np.max(region_ct_values)
                min_val = np.min(region_ct_values)

                mean = round(mean_val, 2)
                min_val = round(min_val)
                max_val = round(max_val)
                stddev = round(stddev_val, 2)

                area = round(W * H / 100, 2)

                area_str = f'{area} cm²'
                w_str = f'{W} mm'
                h_str = f'{H} mm'
                ans = [
                    {
                        'attr': 'Mean',
                        'value': mean
                    },
                    {
                        'attr': 'Min',
                        'value': min_val
                    },
                    {
                        'attr': 'Max',
                        'value': max_val
                    },
                    {
                        'attr': 'StdDev',
                        'value': stddev
                    },
                    {
                        'attr': 'Area',
                        'value': area_str
                    },
                    [
                        {
                            'attr': 'W',
                            'value': w_str
                        },
                        {
                            'attr': 'H',
                            'value': h_str
                        },
                    ]
                ]
                return ans
            case EnumShape.line.value:
                dis = self.get_distance(points[0], points[1])
                desc = f'{dis} mm'
            case EnumShape.angle.value:
                angle = self.get_angle(points)
                desc = f'{angle} °'
        return desc

    def get_distance(self, point0, point1):
        delta_x = abs(point0[0] - point1[0])
        delta_y = abs(point0[1] - point1[1])
        spacing = self.volume_info.get_spacing(self.view_type)
        x_mm_per_px, y_mm_per_px = spacing
        distance = math.sqrt((delta_x / x_mm_per_px) ** 2 + (delta_y / y_mm_per_px) ** 2)
        return round(distance / 10, 2)

    def get_angle(self, points):
        if len(points) != 3:
            return ''
        vector1 = tuple(map(lambda x, y: x - y, points[0], points[1]))
        vector2 = tuple(map(lambda x, y: x - y, points[2], points[1]))
        spacing = self.volume_info.get_spacing(self.view_type)
        actual_vector1 = tuple(vector1[i] * spacing[i] for i in range(len(vector1)))
        actual_vector2 = tuple(vector2[i] * spacing[i] for i in range(len(vector1)))
        return str(calc_vector_angle(actual_vector1, actual_vector2))
